using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using AElf.Kernel;
using AElf.OS.Network.Application;
using AElf.OS.Network.Helpers;
using AElf.OS.Network.Infrastructure;
using AElf.OS.Network.Metrics;
using AElf.OS.Network.Protocol.Types;
using AElf.WebApp.Application.Net.Dto;
using AElf.WebApp.Web;
using Google.Protobuf.WellKnownTypes;
using Microsoft.Extensions.Options;
using Moq;
using Shouldly;
using Xunit;
using Xunit.Abstractions;

namespace AElf.WebApp.Application.Net.Tests;

public sealed class NetAppServiceTest : WebAppTestBase
{
    private readonly BasicAuthOptions _basicAuthOptions;
    private readonly IPeerPool _peerPool;
    private readonly IReconnectionService _reconnectionService;

    public NetAppServiceTest(ITestOutputHelper outputHelper) : base(outputHelper)
    {
        _peerPool = GetRequiredService<IPeerPool>();
        _reconnectionService = GetRequiredService<IReconnectionService>();
        _basicAuthOptions = GetRequiredService<IOptionsMonitor<BasicAuthOptions>>().CurrentValue;
    }

    private IPeer BuildPeer(string ipAddress, string pubkey, Timestamp connectionTime, bool isInbound)
    {
        var connectionInfo = new PeerConnectionInfo
        {
            Pubkey = pubkey,
            ProtocolVersion = KernelConstants.ProtocolVersion,
            ConnectionTime = connectionTime,
            IsInbound = isInbound
        };

        var peerMock = new Mock<IPeer>();
        peerMock.SetupGet(p => p.Info).Returns(connectionInfo);

        AElfPeerEndpointHelper.TryParse(ipAddress, out var ip);

        peerMock.SetupGet(p => p.RemoteEndpoint).Returns(ip);
        peerMock.Setup(p => p.GetRequestMetrics()).Returns(new Dictionary<string, List<RequestMetric>>
            { { "test", new List<RequestMetric> { new() } } });

        return peerMock.Object;
    }

    [Fact]
    public async Task AddPeer_Test()
    {
        var parameters = new Dictionary<string, string>
        {
            { "address", "127.0.0.1:1680" }
        };

        //failed
        await PostResponseAsStringAsync("/api/net/peer", parameters, basicAuth: new BasicAuth
        {
            UserName = BasicAuth.DefaultUserName,
            Password = "123"
        }, expectedStatusCode: HttpStatusCode.Unauthorized);

        _basicAuthOptions.Password = string.Empty;
        _basicAuthOptions.UserName = string.Empty;

        await PostResponseAsync("/api/net/peer", parameters, basicAuth: new BasicAuth
            {
                Password = _basicAuthOptions.Password,
                UserName = _basicAuthOptions.UserName
            }, expectedStatusCode: HttpStatusCode.Unauthorized,
            reason: "User name and password for basic auth should be set");

        _basicAuthOptions.Password = BasicAuth.Default.Password;
        _basicAuthOptions.UserName = BasicAuth.Default.UserName;

        //success
        var response =
            await PostResponseAsObjectAsync<bool>("/api/net/peer", parameters, basicAuth: BasicAuth.Default);
        response.ShouldBeFalse();
    }

    [Fact]
    public async Task RemovePeer_CancelsReconnection_EvenIfPeerNotInPool()
    {
        _reconnectionService.SchedulePeerForReconnection("127.0.0.1:3000");
        await DeleteResponseAsStringAsync("/api/net/peer?address=127.0.0.1:3000", basicAuth: new BasicAuth
        {
            UserName = BasicAuth.DefaultUserName,
            Password = "123"
        }, expectedStatusCode: HttpStatusCode.Unauthorized);

        _basicAuthOptions.Password = string.Empty;
        _basicAuthOptions.UserName = string.Empty;

        await DeleteResponseAsync("/api/net/peer?address=127.0.0.1:3000", basicAuth: new BasicAuth
            {
                UserName = BasicAuth.DefaultUserName,
                Password = "123"
            }, expectedStatusCode: HttpStatusCode.Unauthorized,
            reason: "User name and password for basic auth should be set");

        _basicAuthOptions.Password = BasicAuth.Default.Password;
        _basicAuthOptions.UserName = BasicAuth.Default.UserName;

        await DeleteResponseAsObjectAsync<bool>("/api/net/peer?address=127.0.0.1:3000",
            basicAuth: BasicAuth.Default);
        _reconnectionService.GetPeersReadyForReconnection(null).ShouldBeEmpty();
    }

    [Fact]
    public async Task GetPeers_Test()
    {
        var connectionTime = TimestampHelper.GetUtcNow();
        var ipAddressOne = "192.168.1.1:1680";
        var onePubkey =
            "048f5ced21f8d687cb9ade1c22dc0e183b05f87124c82073f5d82a09b139cc466efbfb6f28494d0a9d7366fcb769fe5436cfb7b5d322a2b0f69c4bcb1c33ac24ad";

        var peerOne = BuildPeer(ipAddressOne, onePubkey, connectionTime, true);
        _peerPool.TryAddPeer(peerOne);

        var ipAddressTwo = "192.168.1.2:1680";
        var twoPubkey =
            "040a7bf44d2c79fe5e270943773783a24eed5cda3e71fa49470cdba394a23832d5c831e233cddebea2720c194dffadd656d4dedf84643818ca77edeee17ad4307a";

        var peerTwo = BuildPeer(ipAddressTwo, twoPubkey, connectionTime, false);
        _peerPool.TryAddPeer(peerTwo);

        var peers = await GetResponseAsObjectAsync<List<PeerDto>>("api/net/peers");

        peers.Count.ShouldBe(2);

        peers.ShouldContain(peer => peer.IpAddress.IsIn(ipAddressOne, ipAddressTwo));
        peers.ShouldContain(peer => peer.ProtocolVersion == KernelConstants.ProtocolVersion);
        peers.ShouldContain(peer => peer.ConnectionTime == connectionTime.Seconds);
        peers.ShouldContain(peer => peer.Inbound);
        peers.ShouldContain(peer => peer.Inbound == false);
        peers.ShouldAllBe(peer => peer.RequestMetrics.Count == 0);
    }

    [Fact]
    public async Task GetPeers_WithMetrics_Test()
    {
        var connectionTime = TimestampHelper.GetUtcNow();
        var ipAddressOne = "192.168.1.1:1680";
        var onePubkey =
            "048f5ced21f8d687cb9ade1c22dc0e183b05f87124c82073f5d82a09b139cc466efbfb6f28494d0a9d7366fcb769fe5436cfb7b5d322a2b0f69c4bcb1c33ac24ad";

        var peerOne = BuildPeer(ipAddressOne, onePubkey, connectionTime, true);
        _peerPool.TryAddPeer(peerOne);

        var peers = await GetResponseAsObjectAsync<List<PeerDto>>("api/net/peers?withMetrics=true");

        peers.Count.ShouldBe(1);

        peers.ShouldContain(peer => peer.IpAddress.IsIn(ipAddressOne));
        peers.ShouldContain(peer => peer.ProtocolVersion == KernelConstants.ProtocolVersion);
        peers.ShouldContain(peer => peer.ConnectionTime == connectionTime.Seconds);
        peers.ShouldContain(peer => peer.Inbound);
        peers.ShouldAllBe(peer => peer.RequestMetrics.Count == 1);
    }

    [Fact]
    public async Task RemovePeer_Test()
    {
        var connectionTime = TimestampHelper.GetUtcNow();
        var ipAddressOne = "192.168.1.1:1680";
        var onePubkey =
            "048f5ced21f8d687cb9ade1c22dc0e183b05f87124c82073f5d82a09b139cc466efbfb6f28494d0a9d7366fcb769fe5436cfb7b5d322a2b0f69c4bcb1c33ac24ad";

        var peerOne = BuildPeer(ipAddressOne, onePubkey, connectionTime, true);
        _peerPool.TryAddPeer(peerOne);

        var ipAddressTwo = "192.168.1.2:1680";
        var twoPubkey =
            "040a7bf44d2c79fe5e270943773783a24eed5cda3e71fa49470cdba394a23832d5c831e233cddebea2720c194dffadd656d4dedf84643818ca77edeee17ad4307a";

        var peerTwo = BuildPeer(ipAddressTwo, twoPubkey, connectionTime, false);
        _peerPool.TryAddPeer(peerTwo);

        var response =
            await DeleteResponseAsObjectAsync<bool>($"/api/net/peer?address={ipAddressOne}",
                basicAuth: BasicAuth.Default);
        response.ShouldBeTrue();

        var peers = await GetResponseAsObjectAsync<List<PeerDto>>("/api/net/peers");
        peers.Count.ShouldBe(1);
        peers.ShouldContain(peer => peer.IpAddress.IsIn(ipAddressTwo));
    }

    [Fact]
    public async Task GetNetWorkInfo_Test()
    {
        var connectionTime = TimestampHelper.GetUtcNow();
        var ipAddressOne = "192.168.1.1:1680";
        var onePubkey =
            "048f5ced21f8d687cb9ade1c22dc0e183b05f87124c82073f5d82a09b139cc466efbfb6f28494d0a9d7366fcb769fe5436cfb7b5d322a2b0f69c4bcb1c33ac24ad";

        var peerOne = BuildPeer(ipAddressOne, onePubkey, connectionTime, true);
        _peerPool.TryAddPeer(peerOne);

        var ipAddressTwo = "192.168.1.2:1680";
        var twoPubkey =
            "040a7bf44d2c79fe5e270943773783a24eed5cda3e71fa49470cdba394a23832d5c831e233cddebea2720c194dffadd656d4dedf84643818ca77edeee17ad4307a";

        var peerTwo = BuildPeer(ipAddressTwo, twoPubkey, connectionTime, false);
        _peerPool.TryAddPeer(peerTwo);

        var peers = await GetResponseAsObjectAsync<List<PeerDto>>("api/net/peers");

        var networkInfo = await GetResponseAsObjectAsync<GetNetworkInfoOutput>("/api/net/networkInfo");
        networkInfo.Version.ShouldBe(typeof(NetApplicationWebAppAElfModule).Assembly.GetName().Version.ToString());
        networkInfo.ProtocolVersion.ShouldBe(KernelConstants.ProtocolVersion);
        networkInfo.Connections.ShouldBe(peers.Count);
    }
}